package com.sunxboy.platform.application;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Level;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;

import com.sunxboy.platform.definition.Application;
import com.sunxboy.platform.definition.ApplicationDefinitionProvider;
import com.sunxboy.platform.definition.NoMain;
import com.sunxboy.platform.log4j.Log4jConfigurator;
import com.sunxboy.platform.logging.Log;
import com.sunxboy.platform.logging.LogFactory;
import com.sunxboy.platform.option.CommandLineArgumentProcessor;
import com.sunxboy.platform.option.CommandLineOptions;
import com.sunxboy.platform.option.Options;

public class ApplicationLauncher {
    private static Log LOG = null;

    private final static String DEFAULT_BT_SHARED = "bootstrapSharedContext.xml";
    private final static String DEFAULT_BT_ALL = "bootstrapAll.xml";
    private final static String DEFAULT_LOG = "logging.xml";
    private final static String DEFAULT_JMX = "jmx.xml";

    private final ApplicationDefinitionProvider appDefProvider;
    private ApplicationContext context = null;
    private static CommandLineOptions commandLineOptions;

    public ApplicationLauncher(final ApplicationDefinitionProvider applicationDefinitionProvider) {
        this.appDefProvider = applicationDefinitionProvider;
    }

    public static void main(String[] args) {

        try {
            loadInitializeProperties();
            initializeBootstrapLogging(null);
            {
                CommandLineArgumentProcessor commandLineArgumentProcessor = new CommandLineArgumentProcessor(args);
                commandLineOptions = commandLineArgumentProcessor.getOptions();
            }
            initializeBootstrapLogging(commandLineOptions);

            final String appName = commandLineOptions.getApplicationId();

            final ApplicationDefinitionProvider selectedProvider = selectApplication(appName);

            ApplicationLauncher appLauncher = new ApplicationLauncher(selectedProvider);
            appLauncher.run(args);

        }
        catch (Throwable e) {
            System.err.println("Exception while instantiating and running the app");
            e.printStackTrace();
            System.exit(25);
        }
    }

    private void run(String[] args) {
        try {

            instantiateAndRun(args);

        }
        catch (Throwable e) {
            final String message = "Exception while instantiating and running the app";
            if (LOG == null) {
                System.err.println(message);
                e.printStackTrace();
            }
            else {
                e.printStackTrace();
                LOG.error(message, e);
            }
            try {
                Thread.sleep(5000);
            }
            catch (Exception ie) {
            }

        }
    }

    private void instantiateAndRun(String[] args) {

        /*
         * Instantiate spring context
         */
        {
            final ApplicationContext coreContext = loadCoreContext(args);
            final String prefix = System.getProperties().getProperty("resource.context");
            final List<Resource> initialResources = new LinkedList<Resource>();

            // load application configuration
            initialResources.addAll(this.appDefProvider.getBeanResources());
            initialResources.add(new ClassPathResource(prefix + DEFAULT_LOG));
            initialResources.add(new ClassPathResource(prefix + DEFAULT_JMX));

            this.context = createContextFromResource(initialResources, coreContext);
        }

        final Application application;
        {

            /*
             * Application does not have a main class. Nothing to do
             */
            if (this.appDefProvider instanceof NoMain) {
                return;
            }

            Map<String, Application> appBeansMap = this.context.getBeansOfType(Application.class);
            Assert.notEmpty(appBeansMap, "No application beans in the application context");

            Assert.isTrue(appBeansMap.size() == 1, "Main application bean not found, The following candidates are available: " + appBeansMap.keySet());
            application = appBeansMap.values().iterator().next();
        }

        final String[] cleanArgs;
        {
            CommandLineArgumentProcessor comProcessor = getSingleBeanOfTypeIncludingAncestors(this.context, CommandLineArgumentProcessor.class);
            cleanArgs = (comProcessor == null ? args : comProcessor.getArguments());
        }

        application.main(cleanArgs);
    }

    private <T> T getSingleBeanOfTypeIncludingAncestors(ApplicationContext context, Class<T> type) {
        Map<String, T> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);

        Assert.isTrue((beans.size() == 1), "One bean of type is required," + type.getName() + ":" + beans.keySet());
        return beans.values().iterator().next();
    }

    private ApplicationContext loadCoreContext(String[] args) {

        final GenericApplicationContext staticApplicationContext;
        {
            final StaticListableBeanFactory beanFactory = new StaticListableBeanFactory();
            beanFactory.addBean("core.cmdOptions", args);
            beanFactory.addBean("core.lifeCycleManager", this);

            staticApplicationContext = new GenericApplicationContext(new DefaultListableBeanFactory(beanFactory));
            staticApplicationContext.refresh();
        }

        final ApplicationContext bootstrapContext;
        {
            final List<Resource> bootstrapResources = new LinkedList<Resource>();
            final String prefix = System.getProperties().getProperty("resource.context");
            bootstrapResources.add(new ClassPathResource(prefix.concat(DEFAULT_BT_ALL)));
            bootstrapResources.add(new ClassPathResource(prefix.concat(DEFAULT_BT_SHARED)));

            bootstrapContext = createContextFromResource(bootstrapResources, staticApplicationContext);
        }

        final ApplicationContext sharedContext;
        {
            final SharedContextLoader sharedContextLoader = findContextLoader(bootstrapContext);
            sharedContext = sharedContextLoader.loadContext(bootstrapContext);
        }

        return sharedContext;
    }

    private SharedContextLoader findContextLoader(ApplicationContext appBotstrapContext) {

        final Map<String, SharedContextLoader> contextLoader = appBotstrapContext.getBeansOfType(SharedContextLoader.class);

        Assert.isTrue((contextLoader.size() == 1), "One Context loader is required!");
        return contextLoader.values().iterator().next();
    }

    private ApplicationContext createContextFromResource(final List<Resource> resources, final ApplicationContext parent) {

        final GenericApplicationContext applicationContext = new GenericApplicationContext();
        final XmlBeanDefinitionReader xmlBeanReader = new XmlBeanDefinitionReader(applicationContext);

        for (Resource beanResource : resources) {
            xmlBeanReader.loadBeanDefinitions(beanResource);
        }

        applicationContext.setParent(parent);
        applicationContext.refresh();
        applicationContext.registerShutdownHook();
        return applicationContext;
    }

    private static ApplicationDefinitionProvider selectApplication(String appIdToRun) {

        ApplicationDefinitionProvider selectedProvider = null;
        Assert.hasText(appIdToRun, "Explicit application id can be specified on command line option: " + Options.APPID);

        final Map<String, ApplicationDefinitionProvider> providerMap = new HashMap<String, ApplicationDefinitionProvider>();
        {
            ServiceLoader<ApplicationDefinitionProvider> appProviders = new ServiceLoader<ApplicationDefinitionProvider>(ApplicationDefinitionProvider.class);
            selectedProvider = processApplicationProviders(appIdToRun, appProviders, providerMap);
        }

        /*
         * Create a list of definition providers
         */
        if (selectedProvider == null) {
            ServiceLoader<? extends ApplicationDefinitionProvider> appProviders =
                    new ServiceLoader<ApplicationDefinitionProvider>(ApplicationDefinitionProvider.class);

            final int providers = providerMap.size();
            selectedProvider = processApplicationProviders(appIdToRun, appProviders, providerMap);

            final int difference = providerMap.size() - providers;
            if (difference != 0) {
                LOG.info(String.format("!!!! %d provider(s) loaded from deprecated service file '%s'", difference, appProviders.getResourceName()));
            }
        }

        Assert.notEmpty(providerMap, "There are no application available in class path: META-INF/services/");
        Assert.notNull(selectedProvider, "Unable to select application to start, ");

        return selectedProvider;
    }

    synchronized protected static void initializeBootstrapLogging(CommandLineOptions commandLineParameters) {
        Log4jConfigurator logConfigurator = new Log4jConfigurator();
        if (commandLineParameters != null) {

            Level logLevel = commandLineParameters.getLogLevel();

            if (logLevel != null) {
                logConfigurator.setDefaultLogLevel(logLevel);
                logConfigurator.initializeBootstrapLogging(true);
            }
        }
        else {
            if (LOG == null) {
                logConfigurator.initializeBootstrapLogging();
            }
        }

        LOG = LogFactory.getLog(ApplicationLauncher.class);
    }

    synchronized private static void loadInitializeProperties() {
        InputStream in = ApplicationLauncher.class.getClassLoader().getResourceAsStream("configuration.properties");
        try {
            System.getProperties().load(in);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            in = null;
        }
    }

    private static ApplicationDefinitionProvider processApplicationProviders(String appIdToRun,
            ServiceLoader<? extends ApplicationDefinitionProvider> appProviders, Map<String, ApplicationDefinitionProvider> providerMap) {
        /**
         * Build a list of app providers
         */
        for (Iterator<? extends ApplicationDefinitionProvider> providersItr = appProviders.iterator(); providersItr.hasNext();) {
            ApplicationDefinitionProvider provider = providersItr.next();
            ApplicationDefinitionProvider duppedProvider = providerMap.put(provider.getAppId(), provider);

            if (duppedProvider != null) {
                LOG.warn("Duplicate Application provider for: '" + provider.getAppId() + "' application id was encountered.");
            }

            if (provider.getAppId().equals(appIdToRun)) {
                return provider;
            }
        }

        return null;
    }
}
